﻿using System;
using System.Collections.Generic;
using System.Text;

namespace Universe
{
    using System.Diagnostics;
    using System.IO;
    using System.Reflection;
    using System.Threading;

    public class CrossInfo
    {
        
        public static bool IsLinux
        {
            get
            {
                return Environment.OSVersion.Platform == PlatformID.Unix
                       && ThePlatform == Platform.Linux;
            }
        }

        
        static Lazy<bool> _IsLinuxOnArm = new Lazy<bool>(() =>
        {
            if (!IsLinux) return false;
            string model;
            GetLinuxCpuInfo(out model);
            bool ret = model != null && model.ToUpper().Contains("ARM");
            if (ret)
                Trace.WriteLine("Workaround activated: Math.Round(decimal...) on ARM");

            return ret;
        });
        
        public static bool IsLinuxOnArm
        {
            get { return _IsLinuxOnArm.Value;  }
        }

        static Lazy<string>  _ProcessorName = new Lazy<string>(() =>
        {
            if (ThePlatform == Platform.Linux)
            {
                return Linux_ProcName();
            }

            else if (ThePlatform == Platform.MacOSX)
                return MacOs_ProcName();

            else
            {
                return Windows_ProcName();
            }
        });

        public static string ProcessorName
        {
            get { return _ProcessorName.Value; }
        }

        public static decimal Round(decimal arg, int digits)
        {
            if (!IsLinuxOnArm) return Math.Round(arg, digits);

            long d = 10;
            switch (digits)
            {
                case 2:
                    d = 100;
                    break;
                case 3:
                    d = 1000;
                    break;
                case 4:
                    d = 10000;
                    break;
                case 5:
                    d = 100000;
                    break;
                case 6:
                    d = 1000000;
                    break;

                default:
                    for (int i = 0; i < digits; i++) d = d*10;
                    break;
            }

            // return ((long)(d * arg)) / (decimal)d;
            return Math.Floor(arg*d + 0.5m)/d;
        }

        static Lazy<bool> _IsMono = new Lazy<bool>(() =>
        {
            return Type.GetType("Mono.Runtime", false) != null;
        });

        public static bool IsMono
        {
            get { return _IsMono.Value; }
        }

        private static void GetLinuxCpuInfo(out string model)
        {
            model = null;
            string procCpuinfo = "/proc/cpuinfo";
            if (!File.Exists(procCpuinfo)) return;
            foreach (var s in EnumLines(procCpuinfo))
            {
                string key;
                string value;
                TrySplit(s, ':', out key, out value);
                if (StringComparer.InvariantCultureIgnoreCase.Equals("model name", key.Trim()))
                {
                    if (value.Length > 0)
                    {
                        model = value;
                        return;
                    }
                }
            }
        }

        public static void HiddenExec
            (
            string command, string args, 
            out string output, out Exception outputException,
            out string error, out Exception errorException,
            out int exitCode
            )
        {
            ProcessStartInfo si = new ProcessStartInfo(command, args)
            {
                CreateNoWindow = true,
                RedirectStandardError = true,
                RedirectStandardOutput = true,
                StandardErrorEncoding = Encoding.UTF8,
                StandardOutputEncoding = Encoding.UTF8,
                WindowStyle = ProcessWindowStyle.Hidden,
                UseShellExecute = false,
            };

            Process p = new Process()
            {
                StartInfo = si,
            };

            // error is written to output xml
            ManualResetEvent outputDone = new ManualResetEvent(false);
            ManualResetEvent errorDone = new ManualResetEvent(false);

            string my_output = null;
            string my_error = null;
            Exception my_outputException = null;
            Exception my_errorException = null;

            Thread t1 = new Thread(() =>
            {
                try
                {
                    my_error = p.StandardError.ReadToEnd();
                    // my_error = DumpToEnd(p.StandardError).ToString();
                }
                catch (Exception ex)
                {
                    my_errorException = ex;
                }
                finally
                {
                    errorDone.Set();
                }
            }) { IsBackground = true };

            Thread t2 = new Thread(() =>
            {
                try
                {
                    my_output = p.StandardOutput.ReadToEnd();
                    // my_output = DumpToEnd(p.StandardOutput).ToString();  
                }
                catch (Exception ex)
                {
                    my_outputException = ex;
                }
                finally
                {
                    outputDone.Set();
                }
            }) { IsBackground = true };

            using (p)
            {
                p.Start();
                t2.Start();
                t1.Start();
                errorDone.WaitOne();
                outputDone.WaitOne();
                p.WaitForExit();
                exitCode = p.ExitCode;
            }

            output = my_output;
            error = my_error;
            outputException = my_outputException;
            errorException = my_errorException;

        }

        private static StringBuilder DumpToEnd(StreamReader rdr)
        {
            StringBuilder b = new StringBuilder();
            while (true)
            {
                string s = rdr.ReadLine();
                if (s == null)
                    break;

                // Console.WriteLine(s);
                b.AppendLine(s);
            }
            return b;
        }

        static void HiddenExec(string command, string args, out string output, out int exitCode)
        {
                        
            ProcessStartInfo si = new ProcessStartInfo(command, args)
            {
                CreateNoWindow = true,
                RedirectStandardError = true,
                RedirectStandardOutput = true,
                StandardErrorEncoding = Encoding.UTF8,
                StandardOutputEncoding = Encoding.UTF8,
                WindowStyle = ProcessWindowStyle.Hidden,
                UseShellExecute = false,
            };


            Process p = new Process() {StartInfo = si};
            using (p)
            {
                p.Start();
                output = p.StandardOutput.ReadToEnd();
                p.WaitForExit();
                exitCode = p.ExitCode;
            }
        }

        // -s: Linux | Darwin
        static string ExecUName(string arg)
        {
            string ret;
            int exitCode;
            HiddenExec("uname", arg, out ret, out exitCode);
            if (ret != null)
                ret = ret.Trim("\r\n\t ".ToCharArray());

            return exitCode == 0 ? ret : ret;
        }

        static string ExecSysCtl(string arg)
        {
            string ret;
            int exitCode;
            HiddenExec("sysctl", arg, out ret, out exitCode);
            if (ret != null)
                ret = ret.Trim("\r\n\t ".ToCharArray());

            return exitCode == 0 ? ret : ret;
        }


        public enum Platform
        {
            Windows,
            Linux,
            MacOSX,
            Unknown
        }

        static Lazy<Platform> _Platform = new Lazy<Platform>(() =>
        {
            if (Environment.OSVersion.Platform == PlatformID.MacOSX)
                return Platform.MacOSX;

            else if (Environment.OSVersion.Platform == PlatformID.Win32NT)
                return Platform.Windows;

            else if (Environment.OSVersion.Platform == PlatformID.Unix)
            {
                var sName = ExecUName("-s");
                if ("Darwin".Equals(sName, StringComparison.InvariantCultureIgnoreCase))
                    return Platform.MacOSX;
                else
                    return Platform.Linux;
            }

            return Platform.Unknown;
        });

        public static Platform ThePlatform
        {
            get { return _Platform.Value; }
        }

        private static string MacOs_ProcName()
        {
            var name = StripDoubleWhitespace(ExecSysCtl("-n machdep.cpu.brand_string") ?? "");
            var cache = ExecSysCtl("-n machdep.cpu.cache.size");
            return name
                   + (!string.IsNullOrEmpty(cache) + "; Cache " + cache);
        }

        private static string Linux_ProcName()
        {
            String name = null, cache = null;
            string fileName = "/proc/cpuinfo";
            if (!File.Exists(fileName))
                return (ExecUName("-m") ?? "").Trim();

            foreach (var line in EnumLines(fileName))
            {
                string key, value;
                TrySplit(line, ':', out key, out value);
                if (key != null)
                {
                    key = key.Trim();
                    if ("model name".Equals(key, StringComparison.InvariantCultureIgnoreCase))
                        name = value;
                    else if ("cache size".Equals(key, StringComparison.InvariantCultureIgnoreCase))
                        cache = value;
                }
            }

            name = name ?? "";
            if (string.IsNullOrEmpty(name))
                name = ExecUName("-m");

            name = StripDoubleWhitespace(name.Trim());
            cache = (cache ?? "").Trim();

            return name + (cache.Length == 0 ? "" : (", Cache " + cache));
        }

        private static unsafe string Windows_ProcName()
        {
/*
            ManagementObjectSearcher mos = new ManagementObjectSearcher("root\\CIMV2", "SELECT * FROM Win32_Processor");
            foreach (ManagementObject mo in mos.Get())
            {
                return mo["Name"];
            }
*/
            return sizeof (IntPtr) == 4 ? "x86" : "x64";
        }

        private static string MacOs_OsName()
        {
            var build = ExecSysCtl("-n kern.osversion");
            return
                "MAC OS X v10."
                + (Environment.OSVersion.Version.Major - 4)
                + (!string.IsNullOrEmpty(build) ? (", Build " + build) : "");
        }

        private static string Linux_OsName()
        {
            string firstLine = null;
            var files = new DirectoryInfo("/etc").GetFiles();
            foreach (FileInfo fileInfo in files)
            {
                if (fileInfo.FullName.ToLower().EndsWith("release"))
                {
                    var lines = File.ReadAllLines(fileInfo.FullName);
                    foreach (var line in lines)
                    {
                        string key, value;
                        TrySplit(line, '=', out key, out value);
                        if ("pretty_name".Equals(key, StringComparison.InvariantCultureIgnoreCase))
                        {
                            string pretty = value.Trim().Trim(new[] {'"'});
                            if (pretty.Length > 0)
                                return pretty;
                        }

                        // SUSE, pretty_name=... is absent
                        if (line.IndexOf('=') == -1 && firstLine == null)
                            firstLine = line;
                    }
                }
            }

            return firstLine;
        }

        static string StripDoubleWhitespace(string arg)
        {
            while (arg.IndexOf("  ") > 0)
                arg = arg.Replace("  ", " ");

            return arg;
        }

        
        static Lazy<string> _OsDisplayName = new Lazy<string>(() =>
        {
            if (ThePlatform == Platform.Linux)
            {
                var s1 = Linux_OsName();
                var s2 = ExecUName("-r");
                return s1 + (!string.IsNullOrEmpty(s2) ? ("; Kernel " + s2) : "");
            }

            else if (ThePlatform == Platform.MacOSX)
                return MacOs_OsName();

            else
            {
                return Environment.OSVersion.VersionString;
            }

        });

        public static string OsDisplayName
        {
            get { return _OsDisplayName.Value; }
        }

        private static void TrySplit(string line, char separator, out string key, out string value)
        {
            key = value = null;
            int pos = line.IndexOf(separator);
            if (pos > 0)
            {
                key = line.Substring(0, pos);
                value = pos < line.Length - 1 ? line.Substring(pos + 1, line.Length - pos - 1) : "";
            }
        }

        static Lazy<string> _RuntimeDisplayName = new Lazy<string>(() =>
        {
            if (IsMono)
            {
                MethodInfo method = Type.GetType("Mono.Runtime", false).GetMethod("GetDisplayName", BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.ExactBinding);
                if (method != null)
                    return 
                        "CLR " + Environment.Version + "; "
                        + "Mono " + method.Invoke(null, new object[0]);
            }

            return "Net " + Environment.Version;
        });

        public static string RuntimeDisplayName
        {
            get { return _RuntimeDisplayName.Value; }
        }

        private static IEnumerable<string> EnumLines(string fileName)
        {
            using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
            using (StreamReader rdr = new StreamReader(fs, Encoding.UTF8))
            {
                string s;
                while ((s = rdr.ReadLine()) != null)
                    yield return s;
            }
        }

        private class Lazy<T>
        {
            public delegate T Function();

            private Function _Function;

            public Lazy(Function function)
            {
                _Function = function;
            }

            private T _Value;
            private /*static*/ readonly object Sync = new object();
            private bool _Ready = false;

            public T Value
            {
                get
                {
                    if (!_Ready)
                        lock(Sync)
                            if (!_Ready)
                            {
                                _Value = _Function();
                                _Ready = true;
                            }

                    return _Value;
                }
            }
        }

        private static bool _UnitTraceAttached;
        static readonly object SyncAttachUnitTrace = new object();
        public static void AttachUnitTrace()
        {
            lock (SyncAttachUnitTrace)
            {
                if (!HasDefaultTraceListener())
                {
                    Trace.Listeners.Add(new DefaultTraceListener());
                }

                if (Environment.OSVersion.Platform == PlatformID.Unix)
                {
                    // mac os x is also goes here
                    ConfigConsoleOnUnixTraceListener();
                }

                if (!_UnitTraceAttached)
                {
                    _UnitTraceAttached = true;
                    IsLinuxOnArm.ToString();
                    Trace.WriteLine("");
                    Trace.WriteLine("Platform .......... " + CrossInfo.ThePlatform);
                    Trace.WriteLine("Is Linux on Arm ... " + CrossInfo.IsLinuxOnArm);
                    Trace.WriteLine("Runtime ........... " + CrossInfo.RuntimeDisplayName);
                    Trace.WriteLine("OS ................ " + CrossInfo.OsDisplayName);
                    Trace.WriteLine("CPU ............... " + CrossInfo.ProcessorName);
                    Trace.WriteLine("");

                }
            }
        }

        static bool HasDefaultTraceListener()
        {
            foreach (var listener in Trace.Listeners)
                if (listener is DefaultTraceListener)
                    return true;

            return false;
        }

        private static void ConfigConsoleOnUnixTraceListener()
        {
            string consoleUnix = "Console@Unix";
            bool isPresent = false;
            foreach (var listener in Trace.Listeners)
            {
                TextWriterTraceListener l = listener as TextWriterTraceListener;
                if (l != null && l.Name == consoleUnix)
                    isPresent = true;
            }

            if (!isPresent)
            {
                TextWriterTraceListener tl = new TextWriterTraceListener(Console.Out, consoleUnix);
                Trace.Listeners.Add(tl);
            }
        }

    }

    public class UnitLog
    {
        private static StringBuilder TraceBuffer = new StringBuilder();
        
        public static void Write(string info)
        {
            Console.Write(info);
            ToBuffer(info);
        }

        public static void Write(string format, params object[] args)
        {
            var value = string.Format(format, args);
            Console.Write(value);
            ToBuffer(value);

        }

        public static void Label(string label)
        {
            Console.Write(label);
            ToBuffer(label);
        }

        public static void Value(string value)
        {
            var f = Console.ForegroundColor;
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.Write(value);
            Console.ForegroundColor = f;
            ToBuffer(value);
        }

        static void ToBuffer(string portion)
        {
            string nl = Environment.NewLine;
            for (int i = 0; i < portion.Length; i++)
            {
                if (i + nl.Length <= portion.Length
                    && portion.Substring(i, nl.Length) == nl)
                {
                    i += nl.Length - 1;
                    Trace.WriteLine(TraceBuffer.ToString());
                    TraceBuffer.Length = 0;

                }
                else
                {
                    TraceBuffer.Append(portion.Substring(i, 1));
                }
            }
        }

        static UnitLog()
        {
            if (!HasDefaultTraceListener())
            {
                Trace.Listeners.Add(new DefaultTraceListener());
            }

            if (Environment.OSVersion.Platform == PlatformID.Unix)
            {
                ConfigConsoleOnUnixTraceListener();
            }

            Trace.WriteLine("Platform: " + CrossInfo.ThePlatform);
            Trace.WriteLine("Is Linux on Arm: " + CrossInfo.IsLinuxOnArm);
            Trace.WriteLine("Runtime: " + CrossInfo.RuntimeDisplayName);
            Trace.WriteLine("OS: " + CrossInfo.OsDisplayName);
        }

        static bool HasDefaultTraceListener()
        {
            foreach (var listener in Trace.Listeners)
                if (listener is DefaultTraceListener)
                    return true;

            return false;
        }

        private static bool ConfigConsoleOnUnixTraceListener()
        {
            string consoleUnix = "Console@Unix";
            foreach (var listener in Trace.Listeners)
            {
                TextWriterTraceListener l = listener as TextWriterTraceListener;
                if (l != null && l.Name == consoleUnix)
                    return true;
            }

            TextWriterTraceListener tl = new TextWriterTraceListener(Console.Out, consoleUnix);
            Trace.Listeners.Add(tl);
            return false;
        }
    }
}